#!/usr/bin/env python3
#
# Copyright 2015 Opera Software ASA. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Scan for URLs from Eddystone and UriBeacon bluetooth beacons.
"""

import re
import signal
import subprocess
import sys
import time

import argparse

parser = argparse.ArgumentParser(prog='scan-for-urls', description= __doc__)

parser.add_argument('-s','--single', action='store_true',
                    help='Perform a single scan and output all the urls found without duplicates.')

parser.add_argument("-v", "--verbose", action='store_true',
                    help='Print lots of debug output.')

options = parser.parse_args()

schemes = [
        "http://www.",
        "https://www.",
        "http://",
        "https://",
        ]

extensions = [
        ".com/", ".org/", ".edu/", ".net/", ".info/", ".biz/", ".gov/",
        ".com", ".org", ".edu", ".net", ".info", ".biz", ".gov",
        ]


def verboseOutput(text = ""):
    if options.verbose:
        sys.stderr.write(text + "\n")


def decodeUrl(encodedUrl):
    """
    Decode a url encoded with the Eddystone (or UriBeacon) URL encoding scheme
    """

    decodedUrl = schemes[encodedUrl[0]]
    for c in encodedUrl[1:]:
        if c <= 0x20:
            decodedUrl += extensions[c]
        else:
            decodedUrl += chr(c)

    return decodedUrl


def onUrlFound(url):
    """
    Called by onPacketFound, if the packet contains a url.
    """

    sys.stdout.write(url)
    sys.stdout.write("\n")
    sys.stdout.flush()


foundPackets = set()

def onPacketFound(packet):
    """
    Called by the scan function for each beacon packets found.
    """

    data = bytearray.fromhex(packet)

    if options.single:
        tmp = packet[:-3]
        if tmp in foundPackets:
            return
        foundPackets.add(tmp)

    # Eddystone
    if len(data) >= 20 and data[19] == 0xaa and data[20] == 0xfe:
        serviceDataLength = data[21]
        frameType = data[25]

        # Eddystone-URL
        if frameType == 0x10:
            verboseOutput("Eddystone-URL")
            onUrlFound(decodeUrl(data[27:22 + serviceDataLength]))
        elif frameType == 0x00:
            verboseOutput("Eddystone-UID")
        elif frameType == 0x20:
            verboseOutput("Eddystone-TLM")
        else:
            verboseOutput("Unknown Eddystone frame type: {}".format(frameType))

    # UriBeacon
    elif len(data) >= 20 and data[19] == 0xd8 and data[20] == 0xfe:
        serviceDataLength = data[21]
        verboseOutput("UriBeacon")
        onUrlFound(decodeUrl(data[27:22 + serviceDataLength]))

    else:
        verboseOutput("Unknown beacon type")

    verboseOutput(packet)
    verboseOutput()


def scan(duration = None):
    """
    Scan for beacons. This function scans for [duration] seconds. If duration
    is set to None, it scans until interrupted.
    """

    subprocess.call(["sudo", "-v"])
    subprocess.call("sudo hciconfig hci0 reset", shell = True, stdout = subprocess.DEVNULL)
    
    lescan = subprocess.Popen(
            ["sudo", "-n", "hcitool", "lescan", "--duplicates"],
            stdout = subprocess.DEVNULL)

    dump = subprocess.Popen(
            ["sudo", "-n", "hcidump", "--raw"],
            stdout = subprocess.PIPE)

    packet = None
    try:
        startTime = time.time()
        for line in dump.stdout:
            line = line.decode()
            if line.startswith("> "):
                if packet: onPacketFound(packet)
                packet = line[2:].strip()
            elif line.startswith("< "):
                if packet: onPacketFound(packet)
                packet = None
            else:
                if packet: packet += " " + line.strip()

            if duration and time.time() - startTime > duration:
                break

    except KeyboardInterrupt:
        pass

    subprocess.call(["sudo", "kill", str(dump.pid), "-s", "SIGINT"])
    subprocess.call(["sudo", "-n", "kill", str(lescan.pid), "-s", "SIGINT"])


try:
    if options.single:
        scan(3)
    else:
        scan()
except Exception as e:
    sys.stderr.write("Exception: " + str(e) + "\n")
    exit(1)
